/**
 * Copyright (c) 2010-2011 Pierre LABATUT
 *
 * This file is part of multiple-track-player project.
 *
 * multiple-track-player is free software: you can redistribute it and/or modify it
 * under the terms of its license which may be found in the accompanying
 * LICENSE file or <http://code.google.com/p/multiple-track-player/>.
 */
package multitrack.preload;

import java.io.IOException;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.SwingUtilities;

/**
 * Bind a mix audio buffer to a line and provide play pause seek services.
 * Each instance has a slave thread.
 * 
 * @author Pierre LABATUT
 */
public class AudioPlayer {

    static void initialize(final AudioPlayerView apv, final List<String> audioFiles) {
        /**
         * This executor execute the initialization task for every URL and then 
         * execute production task that read audio data till the end.
         */
        final Executor initializationExecutor = Executors.newCachedThreadPool();

        final Runnable runnable = new Runnable() {

            @Override
            public void run() {
                final StringWriter sw = new StringWriter();
                final List<HeapAudioBuffer> buffers = new ArrayList<HeapAudioBuffer>();
                final List<AudioProducer> producers = new ArrayList<AudioProducer>();
                for (String urlString : audioFiles) {
                    try {
                        // Create a producer to read the audio at this URL to produce PCM
                        // this takes time because the file header has to be read which imply
                        // resolving URL and reading several ko of data for each file.
                        final AudioProducer producer = new AudioProducer(new URL(urlString), 4096);
                        producers.add(producer);

                        final HeapAudioBuffer buffer = producer.buffer();
                        buffers.add(buffer);


                    } catch (MalformedURLException ex) {
                        sw.write("Failed to load '" + urlString + "' because it is a malformed URL.\n");
                    } catch (UnsupportedAudioFileException ex) {
                        sw.write("Failed to load '" + urlString + "' because it aims at an unsupported file format.\n");
                    } catch (IOException ex) {
                        sw.write("Failed to load '" + urlString + "' because the file content is not accessible.\n");
                    }
                }

                if (buffers.isEmpty()) {
                    sw.write("No audio file has been loaded. The player will have no loaded track.\n");
                }
                if (sw.toString().isEmpty() == false) {
                    ErrorDialog.report(sw.toString());
                }

                final MixAudioBuffer mix = new MixAudioBuffer(buffers);
                /**
                 * Notify the view that a mix is available, so that track can be displayed
                 */
                SwingUtilities.invokeLater(new Runnable() {

                    @Override
                    public void run() {
                        apv.setMix(mix);
                    }
                });

                try {
                    final SourceDataLine line = AudioSystem.getSourceDataLine(mix.format());
                    line.open();
                    final AudioPlayer player = new AudioPlayer(mix, line);

                    /**
                     * Notify the view that a player is available, to enable the playback button
                     */
                    SwingUtilities.invokeLater(new Runnable() {

                        @Override
                        public void run() {
                            apv.setPlayer(player);
                        }
                    });


                    for (AudioProducer producer : producers) {
                        initializationExecutor.execute(producer);
                    }
                } catch (LineUnavailableException ex) {
                    ErrorDialog.report(ex);
                }
            }
        };

        initializationExecutor.execute(runnable);
    }
    private final ExecutorService playThread = Executors.newFixedThreadPool(1, new ThreadFactory() {

        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "Audio Player");
        }
    });
    private final List<AudioPlayerView> views = new CopyOnWriteArrayList<AudioPlayerView>();
    private final AudioBuffer audioBuffer;
    private final SourceDataLine line;
    private Future<?> playing;
    private int pauseCursorPosition = 0;
    private int startBufferPosition = 0;
    private int startLinePosition = 0;

    public AudioPlayer(final AudioBuffer buffer, final SourceDataLine line) {
        if (buffer == null) {
            throw new IllegalArgumentException();
        }
        if (line == null) {
            throw new IllegalArgumentException();
        }
        this.audioBuffer = buffer;
        this.line = line;
    }

    synchronized int playCursorPosInFrame() {
        return isPlaying() ? startBufferPosition + line.getFramePosition() - startLinePosition : pauseCursorPosition;
    }

    synchronized public boolean isPlaying() {
        return playing != null;
    }

    public void addView(AudioPlayerView view) {
        views.add(view);
    }

    synchronized public void seek(int framePosition) {
        System.out.println(framePosition);
        int validFramePosition = Math.max(0, Math.min(audioBuffer.samples() / audioBuffer.format().getChannels(), framePosition));
        if (isPlaying()) {
            stop();
            play(validFramePosition);
        } else {
            pauseCursorPosition = validFramePosition;
        }
    }

    synchronized public void play(boolean doPlay) {

        if (doPlay == isPlaying()) {
            return;
        }
        if (doPlay) {
            play(pauseCursorPosition);
        } else {
            stop();
        }
    }

    synchronized private void play(final int startAtFrame) {
        if (isPlaying() == true) {
            throw new IllegalStateException();
        }

        // Move to playing state
        pauseCursorPosition = -1;// Useless
        startLinePosition = line.getFramePosition();
        startBufferPosition = startAtFrame;
        System.out.println("start");

        playing = playThread.submit(new PlayThreadRunnable(this, line, audioBuffer, startAtFrame));
        for (AudioPlayerView view : views) {
            view.notifyPlay();
        }
    }

    synchronized public void stop() {
        if (isPlaying() == false) {
            throw new IllegalStateException();
        }
        // Stop the line, and mak it refuse any input (causes an exception on writer thread)
        line.stop();

        // Move to paused state
        pauseCursorPosition = playCursorPosInFrame();
        startLinePosition = -1;// Useless
        startBufferPosition = -1;// Useless


        // flush affect getFramePosition, get information first
        line.flush();

        // Stop the play thread
        if (playing != null && playing.isDone() == false) {
            playing.cancel(true);// Cancel eventual wait for audio data
        }

        try {
            if (playing != null) {
                playing.get();//Wait for termination
            }
        } catch (CancellationException ex) {
        } catch (InterruptedException ex) {
        } catch (ExecutionException ex) {
        }
        playing = null;

        for (AudioPlayerView view : views) {
            view.notifyStop();
        }
        // Reflush is not necessary cause no data coud be writen since line stop
        // line.flush();
    }
}
